{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Naive Bayes - Trabalho\n",
    "\n",
    "## Questão 1\n",
    "\n",
    "Implemente um classifacor Naive Bayes para o problema de predizer a qualidade de um carro. Para este fim, utilizaremos um conjunto de dados referente a qualidade de carros, disponível no [UCI](https://archive.ics.uci.edu/ml/datasets/car+evaluation). Este dataset de carros possui as seguintes features e classe:\n",
    "\n",
    "** Attributos **\n",
    "1. buying: vhigh, high, med, low\n",
    "2. maint: vhigh, high, med, low\n",
    "3. doors: 2, 3, 4, 5, more\n",
    "4. persons: 2, 4, more\n",
    "5. lug_boot: small, med, big\n",
    "6. safety: low, med, high\n",
    "\n",
    "** Classes **\n",
    "1. unacc, acc, good, vgood\n",
    "\n",
    "## Questão 2\n",
    "Crie uma versão de sua implementação usando as funções disponíveis na biblioteca SciKitLearn para o Naive Bayes ([veja aqui](http://scikit-learn.org/stable/modules/naive_bayes.html)) \n",
    "\n",
    "## Questão 3\n",
    "\n",
    "Analise a acurácia dos dois algoritmos e discuta a sua solução."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Questão 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Libraries\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.naive_bayes import MultinomialNB, GaussianNB\n",
    "from sklearn.cross_validation import train_test_split\n",
    "from sklearn.preprocessing import LabelEncoder\n",
    "from sklearn.metrics import accuracy_score, classification_report"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# Creating a class for the Naive Bayes\n",
    "class NaiveBayes:\n",
    "    def __init__(self):\n",
    "        ''' Default Constructor '''\n",
    "        self.lEncoder = LabelEncoder()\n",
    "        self.X = None; self.y = None\n",
    "        self.classProb = None; self.likeTable = {}\n",
    "    \n",
    "    def separateByClass(self):\n",
    "        ''' This functions separates all the dataset indexing dictionaries by the classes '''\n",
    "        separated = {}\n",
    "        for i in range(len(self.y)):\n",
    "            if (self.y[i] not in separated):\n",
    "                separated[self.y[i]] = []\n",
    "            separated[self.y[i]].append(self.X[i])\n",
    "        return separated\n",
    "    \n",
    "    def makeLikeTable(self):\n",
    "        ''' This functions counts the occurences of each attribute based on the classes\n",
    "        and construct the Likelihood table (in this case a Dictionary) calculating all\n",
    "        the propers probabilities '''\n",
    "        sepClass = self.separateByClass()\n",
    "        classSizes = [len(sepClass[i]) for i in sepClass.keys()] \n",
    "        self.classProb = np.array(classSizes) / sum(classSizes)\n",
    "        \n",
    "        self.likeTable = {}\n",
    "        for label in sepClass.keys():\n",
    "            aux = np.column_stack(sepClass[label])\n",
    "            for attribute,idx in zip(aux, range(len(aux))):\n",
    "                counts = np.asarray(np.unique(attribute, return_counts=True)).T\n",
    "                for i in range(4):\n",
    "                    self.likeTable[(label, idx, i)] = 0\n",
    "                for count_it in counts:\n",
    "                    self.likeTable[(label, idx, count_it[0])] = count_it[1] / len(sepClass[label])\n",
    "\n",
    "    def calculateProbability(self, inputVector):\n",
    "        ''' Utilizes the maximum likelihood estimation to calculate the probabilty of\n",
    "        each row in inputVector belongs to each possible class '''\n",
    "        sepClass = self.separateByClass()\n",
    "        \n",
    "        probabilities = {}\n",
    "        for label,_ in sepClass.items():\n",
    "            probabilities[label] = self.classProb[label]\n",
    "            for i in range(len(inputVector)):\n",
    "                probabilities[label] *= self.likeTable[label, i, inputVector[i]]\n",
    "                \n",
    "        return probabilities\n",
    "\n",
    "    def fit(self, X_train, y_train):\n",
    "        ''' Assign the training data and calls the Likelihood Table creator '''\n",
    "        self.X = X_train\n",
    "        self.y = y_train\n",
    "        \n",
    "        self.makeLikeTable()\n",
    "    \n",
    "    def predict(self, inputArray):\n",
    "        ''' Return a list of predictions for each row in inputArray correspondent to\n",
    "        the label of the class with the maximum probability '''\n",
    "        predictions = []\n",
    "        for row in inputArray:\n",
    "            probabilities = self.calculateProbability(row)\n",
    "            predictions.append(max(probabilities, key=probabilities.get))\n",
    "        return predictions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Questão 2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# Carregando o Dataset\n",
    "data = pd.read_csv(\"car.data\", header=None)\n",
    "\n",
    "# Transformando as variáveis categóricas em valores discretos contáveis\n",
    "# Apesar de diminuir a interpretação dos atributos, essa medida facilita bastante a vida dos métodos de contagem\n",
    "for i in range(0, data.shape[1]):\n",
    "    data.iloc[:,i] = LabelEncoder().fit_transform(data.iloc[:,i])\n",
    "\n",
    "# Separação do Conjunto de Treino e Conjunto de Teste (80%/20%)\n",
    "X_train, X_test, y_train, y_test = train_test_split(data.iloc[:,:-1], data.iloc[:,-1], test_size=0.2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Multinomial Naive Bayes (SKlearn Version)\n",
      "Total Accuracy: 0.684971098265896%\n",
      "\n",
      "Classification Report:\n",
      "             precision    recall  f1-score   support\n",
      "\n",
      "      unacc       1.00      0.01      0.02        83\n",
      "        acc       0.00      0.00      0.00        12\n",
      "       good       0.68      1.00      0.81       236\n",
      "      vgood       0.00      0.00      0.00        15\n",
      "\n",
      "avg / total       0.71      0.68      0.56       346\n",
      "\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/petcomp/anaconda3/lib/python3.5/site-packages/sklearn/metrics/classification.py:1113: UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples.\n",
      "  'precision', 'predicted', average, warn_for)\n"
     ]
    }
   ],
   "source": [
    "# Implementação do Naive Bayes Multinomial do SKLearn\n",
    "# O Naive Bayes Multinomial é a implementação do SKLearn que funciona para variáveis discretas,\n",
    "# ao invés de utilizar o modelo da função gaussiana (a classe GaussianNB faz dessa forma)\n",
    "clf = MultinomialNB()\n",
    "clf.fit(X_train.values, y_train.values)\n",
    "\n",
    "y_pred = clf.predict(X_test.values)\n",
    "\n",
    "# Impressão dos Resultados\n",
    "print(\"Multinomial Naive Bayes (SKlearn Version)\")\n",
    "print(\"Total Accuracy: {}%\".format(accuracy_score(y_true=y_test, y_pred=y_pred)))\n",
    "\n",
    "print(\"\\nClassification Report:\")\n",
    "print(classification_report(y_true=y_test, y_pred=y_pred, target_names=[\"unacc\", \"acc\", \"good\", \"vgood\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Questão 3"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Naive Bayes (My Version :D)\n",
      "Total Accuracy: 0.8497109826589595%\n",
      "\n",
      "Classification Report:\n",
      "             precision    recall  f1-score   support\n",
      "\n",
      "      unacc       0.68      0.71      0.69        83\n",
      "        acc       0.50      0.17      0.25        12\n",
      "       good       0.91      0.95      0.93       236\n",
      "      vgood       1.00      0.53      0.70        15\n",
      "\n",
      "avg / total       0.84      0.85      0.84       346\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Utilização da minha função própria de Naive Bayes para o mesmo conjunto de dados\n",
    "nBayes = NaiveBayes()\n",
    "nBayes.fit(X_train.values, y_train.values)\n",
    "y_pred = nBayes.predict(X_test.values)\n",
    "\n",
    "# Impressão dos Resultados\n",
    "print(\"Naive Bayes (My Version :D)\")\n",
    "print(\"Total Accuracy: {}%\".format(accuracy_score(y_true=y_test, y_pred=y_pred)))\n",
    "\n",
    "print(\"\\nClassification Report:\")\n",
    "print(classification_report(y_true=y_test, y_pred=y_pred, target_names=[\"unacc\", \"acc\", \"good\", \"vgood\"]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Discussão\n",
    "\n",
    "### Algoritmo\n",
    "Na minha implementação, assim como no SKLearn, existem duas funções principais: <b>.fit()</b> e <b>.predict()</b>.\n",
    "\n",
    "A função <b>.fit()</b> é responsável por receber os dados de Treino e, através deles, criar as tabelas de Likelihood para cada atributo. Essa tabela é criada através da função <i>.makeLikeTable()</i> que, basicamente, separa todos os exemplos por classe e, para cada classe, realiza a contagem de frequência de cada atributo e calcula as devidas probabilidades. A tabela Likelihood do meu classificador é indexado pela classe, índice do atributo e valor do atributo. Logo, ele possui as probabilidades:\n",
    "\n",
    "$$ P(X_{i}=x\\ |\\ C_{j})$$\n",
    "\n",
    "onde $X_i$ é o atributo de índice $i$, $x$ é o valor que ele assume e $C_j$ é a classe de índice $j$.\n",
    "\n",
    "\n",
    "A função <b>.predict()</b> é responsável por receber os diversos inputs (exemplos para serem classificados) e retornar o label da classe com maior probabilidade para tais inputs. Ele utiliza da função <i>.calculateProbability()</i> para calcular a probabilidade:\n",
    "\n",
    "$$ P(C\\ |\\ X) = P(C) \\prod_i P(X_{i}\\ |\\ C)) $$\n",
    "\n",
    "que é a probabilidade de maximum likelihood definida para a classe C, dado os atributos X. A resposta da predição é, então, a classe para qual o valor da probabilidade acima é máxima.\n",
    "\n",
    "### Resultados\n",
    "Podemos notar que o algoritmo possui um resultado relativamente satisfatório. Apesar de tomar certas suposições bem rígidas, alguns casos realmente caem sobre os casos contemplados pelo Naive Bayes, e a classificação se mostra proveitosa. \n",
    "\n",
    "Todavia, é perceptível que esse algoritmo <b>depende bastante da frequência relativa de cada class.</b> Se olharmos na equação acima, podemos perceber que a probabilidade de maximum likelihood é proporcional à probabilidade marginal das classes. Por esse motivo, classes que possuem uma frequência muito maior que as outras terão uma probabilidade maior, e irão influenciar mais no resultado da probabilidade final. Por esse motivo, podemos ver que as classes com menos exemplares (\"acc\" e \"vgood\") acabam não tendo uma performance muito boa. Esse fato é ainda mais perceptível no Multinomial NaiveBayes, implementado pelo SKLearn. Por utilizar diferentes métodos de contagem, ele possui um melhor custo computacional, mas acaba subestimando as probabilidades das classes menos frequentes e, por consequência, vemos que as predições caem praticamente todas nas duas classes mais frequentes (\"unacc\" e \"good\")"
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python [default]",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
